/**
 * 
 */
package jvs.vfs.arc;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import jvs.vfs.IFile;
import jvs.vfs.IFileBaseImpl;
import x.java.io.File;

/**
 * @author qiangli
 * 
 */
public class ArchiveFileImpl extends IFileBaseImpl {
	private static Hashtable<String, String[][]> cache = new Hashtable<String, String[][]>();
	private static Hashtable<String, String[]> roots = new Hashtable<String, String[]>();
	public static final String SCHEME = "arc";

	private static String[][] buildMap(ZipFile zf) {
		// fix possible missing parent
		HashSet<String> set = new HashSet<String>(zf.size());
		Enumeration<?> entries = zf.entries();
		for (int i = 0; entries.hasMoreElements(); i++) {
			ZipEntry ze = (ZipEntry) entries.nextElement();
			String n = ze.getName();
			// discard link
			if (n.endsWith(File.LINK_EXT)) {
				continue;
			}
			int len = n.length();
			// remove trailing slash
			if (n.endsWith("/")) {
				n = n.substring(0, len - 1);
			}
			set.add(n);
			// add all parent path
			int idx = 0;
			while (idx < len) {
				idx = n.indexOf("/", idx + 1);
				if (idx < 0) {
					break;
				}
				set.add(n.substring(0, idx));
			}
		}
		//
		String[][] map = new String[set.size()][];
		Iterator<String> it = set.iterator();
		for (int i = 0; it.hasNext(); i++) {
			String n = (String) it.next();
			String[] sa = n.split("/");
			map[i] = sa;
		}
		return map;
	}

	private static ZipEntry getEntry(ZipFile zf, String path) {
		path = path.substring(1);
		ZipEntry e = null;
		// a bug? we have to query twice to get it right (isDirectory)
		e = zf.getEntry(path + "/");
		if (e != null) {
			return e;
		}
		e = zf.getEntry(path);
		return e;
	}

	private static String[] getList(String url, String path) {
		String[][] map = getMap(url);
		String[] pa = path.substring(1).split("/");
		int idx = pa.length;
		//
		HashSet<String> l = new HashSet<String>();
		for (int i = 0; i < map.length; i++) {
			String[] mi = map[i];
			if (startsWith(mi, pa) && mi.length > idx) {
				l.add(mi[idx]);
			}
		}
		return (String[]) l.toArray(new String[0]);
	}

	synchronized private static String[][] getMap(String url) {
		String[][] map = null;
		try {
			map = (String[][]) cache.get(url);
			if (map == null) {
				ZipFile zf = getZipFile(url);
				map = buildMap(zf);
				cache.put(url, map);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return map;
	}

	private static boolean isLocalFile(String url) {
		if (url.startsWith("v:")) {
			return new x.java.io.File(url).isLocal();
		} 
		return true;
	}

	private static java.io.File getNativeFile(String url) throws IOException,
			URISyntaxException {
		java.io.File file = null;
		if (url.startsWith("v:")) {
			file = new x.java.io.File(url).getNativeFile();
		} else if (url.startsWith("/")) {
			file = new java.io.File(url);
		} else if (url.startsWith("file:")) {
			file = new java.io.File(url);
		} else {
			throw new IOException(
					"only schemes v:/ and file:/ are supported");
		}
		return file;
	}

	synchronized private static String[] getRootList(String url) {
		String[] root = roots.get(url);
		if (root != null) {
			return root;
		}
		ZipFile zf = getZipFile(url);
		HashSet<String> l = new HashSet<String>();
		Enumeration<?> entries = zf.entries();
		while (entries.hasMoreElements()) {
			ZipEntry entry = (ZipEntry) entries.nextElement();
			String n = entry.getName();
			// discard link
			if (n.endsWith(File.LINK_EXT)) {
				continue;
			}
			int idx = n.indexOf("/", 1);
			if (idx > 0) {
				l.add(n.substring(0, idx));
			}
		}

		root = (String[]) l.toArray(new String[0]);
		roots.put(url, root);
		return root;
	}

	private static ZipFile getZipFile(String url) {
		try {
			java.io.File file = getNativeFile(url);
			String path = file.getPath();

			return new java.util.zip.ZipFile(path);
		} catch (RuntimeException e) {
			e.printStackTrace();
			throw e;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e.getMessage());
		}
	}

	private static boolean isEmpty(String url, String path) {
		String[][] map = getMap(url);

		if (path.equals("/")) {
			return map.length == 0;
		}
		String[] pa = path.substring(1).split("/");
		int idx = pa.length;
		for (int i = 0; i < map.length; i++) {
			String[] mi = map[i];
			if (startsWith(mi, pa) && mi.length > idx) {
				return false;
			}
		}
		return true;
	}

	private static boolean startsWith(String[] sa1, String[] sa2) {
		if (sa1.length < sa2.length) {
			return false;
		}
		for (int i = 0; i < sa2.length; i++) {
			if (sa1[i].equals(sa2[i]) == false) {
				return false;
			}
		}
		return true;
	}

	private boolean exists;

	private boolean isFile;

	private boolean isLocal;

	private long lastModified;

	private long length = -1;

	private String path;

	private String url;

	private ArchiveFileImpl(ArchiveFileImpl parent, String path, ZipEntry entry) {
		super(uri(parent.url, path));
		//
		this.url = parent.url;
		this.path = path;
		this.isLocal = parent.isLocal;
		//
		init(entry);
	}

	private static URI uri(String context, String path) {
		try {
			return URI.create(SCHEME + ":" + context + "!"
					+ URLEncoder.encode(path, "UTF-8"));
		} catch (UnsupportedEncodingException e) {
		}
		return URI.create(SCHEME + ":" + context + "!"
				+ URLEncoder.encode(path)); //
	}

	/**
	 * @param uri
	 */
	public ArchiveFileImpl(URI uri) {
		super(uri);
		//
		String s = uri.toString();
		int lidx = s.lastIndexOf("!");
		path = s.substring(lidx + 1);
		int idx = s.indexOf(":");
		url = s.substring(idx + 1, lidx);

		try {
			path = URLDecoder.decode(path, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		//
		isLocal = isLocalFile(url);
		//
		if (path.equals("/")) {
			lastModified = System.currentTimeMillis();
			length = 0;
			isFile = false;
			exists = true;
			return;
		}
		//
		if (path.endsWith("/")) {
			int len = path.length();
			if (len > 1) {
				path = path.substring(0, len - 1);
			}
		}
		//
		ZipFile zf = getZipFile(url);
		ZipEntry entry = getEntry(zf, path);
		//
		init(entry);
	}

	@Override
	public boolean canRead() {
		return true;
	}

	@Override
	public boolean canWrite() {
		return false;
	}

	@Override
	public boolean create() {
		return false;
	}

	@Override
	public boolean delete() {
		return false;
	}

	@Override
	public boolean exists() {
		return exists;
	}

	@Override
	public InputStream getInputStream() {
		if (isFile == false) {
			return null;
		}
		try {
			ZipFile zf = getZipFile(url);
			ZipEntry entry = getEntry(zf, path);
			InputStream is = zf.getInputStream(entry);
			return is;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public long getLastModified() {
		return lastModified;
	}

	@Override
	public long getLength() {
		return length;
	}

	@Override
	public OutputStream getOutputStream() {
		return null;
	}

	private void init(ZipEntry entry) {
		if (entry != null) {
			lastModified = entry.getTime();
			length = entry.getSize();
			isFile = (entry.isDirectory() == false);
			exists = true;
		} else {
			lastModified = System.currentTimeMillis();
			length = 0;
			isFile = false;
			exists = true;
		}
	}

	@Override
	public boolean isDirectory() {
		return isFile == false;
	}

	@Override
	public boolean isEmpty() {
		return isEmpty(url, path);
	}

	@Override
	public boolean isFile() {
		return isFile;
	}

	@Override
	public boolean isLocal() {
		return isLocal;
	}

	@Override
	public boolean isNative() {
		return (isLocal && path.equals("/"));
	}

	@Override
	public String[] list() {
		if (isFile) {
			return null;
		}
		if (path.equals("/")) {
			return getRootList(url);
		}
		return getList(url, path);
	}

	@Override
	public IFile[] listFiles() {
		String[] l = list();
		if (l == null) {
			return null;
		}
		//
		IFile[] files = new IFile[l.length];
		ZipFile zf = getZipFile(url);
		String pa = path.equals("/") ? "/" : path + "/";
		for (int i = 0; i < l.length; i++) {
			String p = pa + l[i];
			ZipEntry entry = getEntry(zf, p);
			files[i] = new ArchiveFileImpl(this, p, entry);
		}
		return files;
	}

	@Override
	public IFile[] listFiles(int off, int max) {
		String[] l = list();
		if (l == null) {
			return null;
		}
		int len = l.length;
		if (off >= len) {
			return new ArchiveFileImpl[0];
		}
		//
		Arrays.sort(l);
		len = Math.min(len, off + max);
		IFile[] rl = new IFile[len - off];
		ZipFile zf = getZipFile(url);

		String pa = path.equals("/") ? "/" : path + "/";
		for (int i = off; i < len; i++) {
			String p = pa + l[i];
			ZipEntry entry = getEntry(zf, p);
			rl[i - off] = new ArchiveFileImpl(this, p, entry);
		}
		return rl;
	}

	@Override
	public boolean mkdir() {
		return false;
	}

	@Override
	public boolean move(URI uri) {
		return false;
	}
}
